from heapq import heappush, heappop, heapify
import math, random
import parole
import main, ai, interface
from util import *

class Event(object):

    def __init__(self, id, time, args = None, dests = None):
        self.id = id
        self.time = time
        self.args = args
        self.dests = dests and tuple(dests) or ()

    def __cmp__(self, event):
        return cmp(self.time, event.time)

    def __repr__(self):
        return 'Event(%r, %r, args=%r, dests=%r)' % (self.id, self.time,
                self.args, self.dests)

#========================================

class Listener(object):
    def listen(self, event):
        raise NotImplementedError('listen() not overriden.')

#========================================

class StopSchedule(Exception):
    pass

#========================================

class Schedule(object):

    def __init__(self, startTime = 0):
        self.eventHeap = []
        self.time = startTime
        self.__listeners = {}

    def add(self, event):
        if event.time < self.time:
            raise ValueError('Time travel is disallowed, faggo.')
        heappush(self.eventHeap, event)
        if event.id not in ('think', 'burn'):
            parole.debug('Scheduling event %r', event)

    def __call__(self, event):
        self.add(event)

    def remove(self, event):
        parole.debug('Removing event %r', event)
        if event in self.eventHeap:
            self.eventHeap.remove(event)
            heapify(self.eventHeap)       

    def clear(self):
        del self.eventHeap[:]

    def nextEvent(self):
        return self.eventHeap[0]

    def listen(self, id, listener):
        if id not in self.__listeners:
            self.__listeners[id] = []
        self.__listeners[id].append(listener)

    def unlisten(self, id, listener):
        # let exceptions propagate
        self.__listeners[id].remove(listener)

    def advance(self):
        try:
            event = heappop(self.eventHeap)
        except IndexError:
            raise StopSchedule('No more events in Schedule.')

        self.time = event.time

        listeners = []
        if event.dests:
            listeners += event.dests
        if event.id in self.__listeners:
            listeners += self.__listeners[event.id]
        listeners = set(listeners)

        if event.id not in ('think', 'burn'):
            parole.debug('Schedule: sending %r to %r', event, listeners)

        for listener in listeners:
            listener.listen(event)

#=========================================

def visible(obj):
    if not main.player.parentTile:
        return False

    if main.schedule.time == 0:
        return False

    if not obj:
        return False

    if isinstance(obj, tuple):
        if not main.mapFrame.fovObj:
            return True
        return obj in main.mapFrame.visibleTiles

    if isinstance(obj, parole.map.Tile):
        if obj.map is not main.mapFrame.getMap():
            return False
        if not main.mapFrame.fovObj:
            return True
        return (obj.col, obj.row) in main.mapFrame.visibleTiles

    if not obj.parentTile:
        return False
    if obj.parentTile.map is not main.mapFrame.getMap():
        return False

    if not main.mapFrame.fovObj:
        return True

    return (obj.parentTile.col, obj.parentTile.row) in main.mapFrame.visibleTiles

#=========================================

class Obj(parole.map.MapObject, Listener):
    """
    Anything that can be located on a map and handle events.
    """
    
    genders = ['male', 'female', 'neuter']

    def __init__(self, name, layer, shader, blockLOS=False, blockMove=False,
            startsVowel=False, massNoun=False, gender='neuter'):
        super(Obj, self).__init__(layer, shader, blocksLOS=blockLOS,
                blocksMove=blockMove)
        self.name = name
        self.startsVowel = startsVowel
        self.massNoun = massNoun
        if gender not in Obj.genders:
            raise ValueError('bad gender: %s' % gender)
        self.gender = gender

    def __str__(self):
        return self.noun()

    def __repr__(self):
        return '%s <%s>' % (str(self), id(self))

    def noun(self, definite=False, proper=False, capitalized=False):
        """
        Full article+noun phrase. No article if proper==True.
        """
        # TODO: "something" if player is blind
        if not proper:
            if definite:
                article = 'the'
            elif self.massNoun:
                article = 'some'
            elif self.startsVowel:
                article = 'an'
            else:
                article = 'a'
            if capitalized:
                article = article[0].upper() + article[1:]

            return '%s %s' % (article, self.name)
        return capitalized and (self.name[0].upper() + self.name[1:]) or \
                self.name

    def pronounNom(self, capitalized=False):
        """
        Nominative pronoun.
        """
        if self.gender == 'male':
            pro = 'he'
        elif self.gender == 'female':
            pro = 'she'
        else:
            pro = 'it'
        if capitalized:
            pro = pro[0].upper() + pro[1:]
        return pro

    def pronounAcc(self):
        """
        Accusative pronoun.
        """
        if self.gender == 'male':
            return 'him'
        elif self.gender == 'female':
            return 'her'
        else:
            return 'it'

    def pronounGen(self, capitalized=False):
        """
        Genitive/possessive pronoun.
        """
        if self.gender == 'male':
            pro = 'his'
        elif self.gender == 'female':
            pro = 'her'
        else:
            pro = 'its'
        if capitalized:
            pro = pro[0].upper() + pro[1:]
        return pro

    def pronounReflexive(self):
        if self.gender == 'male':
            return 'himself'
        elif self.gender == 'female':
            return 'herself'
        else:
            return 'itself'

    def listen(self, event):
        """
        Handles:
            - "translocate" (obj, dest)
            - "leave tile" (obj, pos)
            - "enter tile" (obj, pos, map)
        """
        map = None
        if self.parentTile:
            map = self.parentTile.map

        if event.id == 'translocate':
            obj, dest = event.args
            if obj is self:
                assert(map)
                # schedule immediate leave/enter events 
                main.schedule.add(Event('leave tile', main.schedule.time,
                        (self, self.pos), dests=[self]))
                main.schedule.add(Event('enter tile', main.schedule.time,
                        (self, dest, map), dests=[self]))
                map[self.pos].remove(self)
                map[dest].add(self)
                map.update()
            return
        #elif event.id == 'leave tile':
        #    parole.debug('got %r', event)
        #    obj, pos = event.args
        #    if obj is self:
        #        assert(map)
        #        parole.debug('leaving tile: %s', self.noun())
        #        #assert(self.pos == pos)
        #        map[pos].remove(self)
        #        map.update()
        #elif event.id == 'enter tile':
        #    obj, pos, map = event.args
        #    if obj is self:
        #        #assert(not self.parentTile)
        #        map[pos].add(self)
        #        map.update()

#=========================================

class Door(Obj):
    """
    Any object that can toggle between blocking and not blocking LOS and
    movement. Automatically does so in response to any 'door' event it
    receives with itself as the first argument. The second argument is one of
    'open', 'close', 'toggle'.
    """
    
    def __init__(self, name, layer, openShader, closedShader, open=False,
            startsVowel=False, massNoun=False, gender='neuter'):
        Obj.__init__(self, name, layer, open and openShader or closedShader,
                blockLOS=not open, blockMove=not open, startsVowel=startsVowel,
                massNoun=massNoun, gender=gender)
        self.openShader = openShader
        self.closedShader = closedShader
        self.opened = open
        if open:
            self.open()
        else:
            self.close()

    def isOpen(self):
        return self.opened

    def isClosed(self):
        return not self.opened

    def open(self):
        t = self.parentTile
        if t:
            t.remove(self)
        self.opened = True
        self.blocksLOS = False
        self.blocksMove = False
        self.shader = self.openShader
        if t:
            t.add(self)
            t.map.update()

        if main.mapFrame and main.mapFrame.fovObj:
            main.mapFrame.touchFOV()

    def close(self):
        t = self.parentTile
        if t:
            t.remove(self)
        self.opened = False
        self.blocksLOS = True
        self.blocksMove = True
        self.shader = self.closedShader
        if t:
            t.add(self)
            t.map.update()

        if main.mapFrame and main.mapFrame.fovObj:
            main.mapFrame.touchFOV()

    def toggleOpen(self):
        if self.isOpen():
            self.close()
        else:
            self.open()

    def listen(self, event):
        """
        Handles:
            "door" (target, "open"/"close"/"toggle")
        """
        Obj.listen(self, event)

        if event.id == 'door' and event.args[0] is self:
            action = event.args[1]
            if action == 'open':
                self.open()
            elif action == 'close':
                self.close()
            elif action == 'toggle':
                self.toggleOpen()


#=========================================

class Item(Obj):
    """
    Something that can be picked up and has a weight.
    """
    def __init__(self, name, layer, shader, weight, startsVowel=False):
        Obj.__init__(self, name, layer, shader, startsVowel=startsVowel)
        self.weight = weight
        self.carrier = None

    def noun(self, definite=False, proper=False, capitalized=False):
        n = Obj.noun(self, definite, proper, capitalized)
        if self.carrier is main.player:
            n += " (%s lbs)" % self.weight
        return n

    def listen(self, event):
        """
        Handles:
        """
        Obj.listen(self, event)

#=========================================

class Wearable(Item):
    """
    Something that can be worn on a wearslot.
    """
    bonusKeys = [
        'maxHPMod', 'strengthMod', 'dexterityMod', 'constitutionMod',
        'perceptionMod', 'speedMod', 'maxStaminaMod', 'staminaRegenMod',
        'capacityMod', 'pvMod', 'evMod', 'hitMod', 'damageSkewMod',
        'maxUnarmedDamageMod', 'minVisibleLightMod',
    ]

    def __init__(self, name, layer, shader, weight, wearSlots,
            wearEnergy, bonuses=None, startsVowel=False):
        Item.__init__(self, name, layer, shader, weight,
                startsVowel=startsVowel)
        self.wearSlots = list(wearSlots)
        self.wearer = None
        self.wearerSlot = None
        self.wearEnergy = wearEnergy
        self.bonuses = dict(zip(self.bonusKeys, (0,)*len(self.bonusKeys)))
        if bonuses:
            if type(bonuses) not in (dict, None):
                raise TypeError('bonuses must be a dict or None')
            else:
                for k,v in bonuses.iteritems():
                    if k not in self.bonusKeys:
                        raise ValueError('Unknown bonus key %r' % k)
            self.bonuses.update(bonuses)

    def bonusString(self):
        bs = ''
        if any(self.bonuses.values()):
            bs = ' ['
            for mod, amount in self.bonuses.iteritems():
                if not amount: continue
                bs += '%s%s%s,' % (mod[:mod.index('Mod')],
                                   amount >= 0 and '+' or '',
                                   amount)
            bs = bs[:-1] + ']'
        return bs

    def noun(self, definite=False, proper=False, capitalized=False):
        # TODO: bonus descriptions
        n = Item.noun(self, definite, proper, capitalized)
        if self.carrier:
            if not self.wearer:
                n+= self.bonusString()
            elif self.wearSlots[0] == 'shield':
                n += "%s, readied" % (self.bonusString(),)
            elif self.wearSlots[0] == 'light':
                #n += "%s, lit" % (self.bonusString(),)
                pass
            else:
                n += "%s, worn on the %s" % (self.bonusString(), self.wearerSlot)
        return n

    def listen(self, event):
        """
        Handles:
            "wear"             (item, creature, slot)
            "remove"           (item, creature, slot)
        """
        Item.listen(self, event)

        if event.id == "wear":
            item, creature, slot = event.args
            if item is self:
                for statname, amount in self.bonuses.iteritems():
                    if not amount:
                        continue
                    parole.debug('%r: increasing %r by %d', self, statname,
                            amount)
                    main.schedule.add(Event('increase stat', main.schedule.time,
                        (creature, statname, amount), dests=[creature]))
                    if creature is main.player:
                        pass
                        # TODO: table of more proper human-readable stat names
                        #baseStatName = statname[:statname.index('Mod')]
                        #main.transient('%s affects your %s by %s' % \
                        #        (item.noun(definite=1, capitalized=1),
                        #            baseStatName, amount))
        if event.id == "remove":
            item, creature, slot = event.args
            if item is self:
                for statname, amount in self.bonuses.iteritems():
                    if not amount:
                        continue
                    main.schedule.add(Event('decrease stat', main.schedule.time,
                        (creature, statname, amount), dests=[creature]))

#=========================================

class Armor(Wearable):
    def __init__(self, name, shader, weight, wearSlots, wearEnergy,
            bonuses=None, startsVowel=False):
        Wearable.__init__(self, name, 15, shader, weight, wearSlots,
            wearEnergy, bonuses, startsVowel)
        # TODO: Materials

    def listen(self, event):
        """
        Handles:
        """
        Wearable.listen(self, event)

#=========================================

class Weapon(Wearable):
    """
    A wearable that uses the verb "wield" rather than "wear", and has to-hit and
    damage stats, and construction materials and modifiers.
    """
    def __init__(self, name, shader, weight, wieldEnergy, attackEnergy,
            damage, bonuses=None, verb='swing', startsVowel=False):

        Wearable.__init__(self, name, 20, shader, weight, ['weapon'],
                wieldEnergy, bonuses, startsVowel)
        self.wieldEnergy = wieldEnergy
        self.attackEnergy = attackEnergy
        self.damage = damage
        self.verb = verb
        # TODO: Materials

    def noun(self, definite=False, proper=False, capitalized=False):
        n = Item.noun(self, definite, proper, capitalized)
        if self.carrier:
            n += " [%s%d, D:%d%s%d, A:%d, W:%d], wielded" % \
                    (self.bonuses['hitMod']>=0 and '+' or '',
                    self.bonuses['hitMod'],
                    self.damage, self.bonuses['damageSkewMod']>=0 and '+' or '',
                    self.bonuses['damageSkewMod'], self.attackEnergy,
                    self.wieldEnergy)
        return n

    def drawDamage(self, attacker, target):
        assert(attacker is self.wearer)
        # subclasses may want to have damage be sensitive to the identity of the
        # attacker and/or target (e.g., + vs evil)
        return random.betavariate(attacker['damageSkew'], 10) * self.damage

    def listen(self, event):
        """
        Handles:
        """
        Wearable.listen(self, event)

#=========================================

class LightSource(Item):
    """
    Something that can start or stop emitting light, which follows it if it
    moves.
    """
    def __init__(self, name, layer, shader, weight, lightEnergy, lightRGB,
            lightIntensity, burnTime, startsVowel=False):

        Item.__init__(self, name, layer, shader, weight, startsVowel)
        self.baseName = name
        self.on = False
        self.lightEnergy = lightEnergy
        self.lightRGB = lightRGB
        self.lightIntensity = lightIntensity
        self.lightSource = parole.map.LightSource(self.lightRGB,
                self.lightIntensity)
        self.burnTime = burnTime
        self.updateName()
        self.enteringInventory = False

    def updateName(self):
        if self.on:
            self.name = 'lit ' + self.baseName
            self.startsVowel = False
        elif self.burnTime > 0:
            self.name = 'unlit ' + self.baseName
            self.startsVowel = True
        else:
            self.name = 'spent ' + self.baseName
            self.startsVowel = False

    def __activate(self, map, pos):
        if self.on:
            return
        self.on = True
        self.lightSource.apply(map, pos)
        map.update()
        if main.mapFrame.fovObj and not main.player.dead:
            main.mapFrame.touchFOV()
        self.updateName()
        self.burnEvent = Event('burn', main.schedule.time + 1000, (self,),
                dests=[self])
        main.schedule.add(self.burnEvent)
        main.schedule.listen('enter tile', self)
        main.schedule.listen('leave tile', self)

        if self.carrier:
            if self.carrier.tileIsBrightEnough(self.carrier.parentTile):
                self.carrier.goUnblind()

    def __deactivate(self, map):
        if not self.on:
            return
        self.on = False
        self.lightSource.remove(map)
        map.update()
        if main.mapFrame.fovObj and not main.player.dead:
            main.mapFrame.touchFOV()
        self.updateName()
        main.schedule.remove(self.burnEvent)
        main.schedule.unlisten('enter tile', self)
        main.schedule.unlisten('leave tile', self)

        if self.carrier:
            if not self.carrier.tileIsBrightEnough(self.carrier.parentTile):
                self.carrier.goBlind()

    def listen(self, event):
        """
        Handles:
            "activate light"    (obj)
            "deactivate light"  (obj)
            "toggle light"      (obj)
            "update light"      (obj, lightRGB, lightIntensity)
            "enter tile"        (obj, pos, map)
            "leave tile"        (obj, pos)
            "burn"              (lightsource)
            "leave inventory"   (item, creature)
            "enter inventory"   (item, creature)
        """
        map = None
        pos = None
        if self.parentTile:
            map = self.parentTile.map
            pos = self.pos
        elif self.carrier:
            if self.carrier.parentTile:
                map = self.carrier.parentTile.map
                pos = self.carrier.pos
        if not map:
            # assume it's the currently visible one
            map = main.mapFrame.getMap()


        Item.listen(self, event)

        if event.id == "leave tile":
            obj, pos = event.args
            if obj is self or obj is self.carrier:
                if self.on and not self.enteringInventory:
                    self.lightSource.remove(map)
                    map.update()
                self.enteringInventory = False

        elif event.id == 'leave inventory':
            item, creature = event.args
            if item is self:
                if self.on:
                    if not map:
                        map = creature.parentTile.map
                    self.lightSource.remove(map)
                    map.update()

        elif event.id == 'burn' and event.args[0] is self:
            self.burnTime -= 1000
            if self.burnTime <= 0:
                self.burnTime = 0
                main.schedule.add(Event('deactivate light', main.schedule.time,
                    (self,), dests=[self]))
            else:
                self.burnEvent = Event('burn', main.schedule.time + 1000, (self,),
                        dests=[self])
                main.schedule.add(self.burnEvent)
                if self.burnTime <= 10000 and \
                        (visible(self) or visible(self.carrier)):
                    main.transient("%s gutters like it's about to go out." % \
                            self.noun(definite=1, capitalized=1), pos)

        elif event.id == "activate light" and event.args[0] is self:
            if self.on:
                # already on
                return
            if visible(self) or visible(self.carrier):
                main.transient('%s lights up.' % self.noun(definite=1,
                    capitalized=1), pos)
            self.__activate(map, pos)

        elif event.id == "deactivate light" and event.args[0] is self:
            if not self.on:
                # already off
                return
            if visible(self) or visible(self.carrier):
                main.transient('%s goes out.' % self.noun(definite=1,
                    capitalized=1), pos)
            self.__deactivate(map)

        elif event.id == 'toggle light' and event.args[0] is self:
            if self.on:
                main.schedule.add(Event('deactivate light', main.schedule.time,
                    (self,), dests=[self]))
            else:
                main.schedule.add(Event('activate light', main.schedule.time,
                    (self,), dests=[self]))

        elif event.id == 'update light':
            light, lightRGB, lightIntensity = event.args
            if light is not self:
                return
            if lightIntensity != self.lightIntensity:
                if self.on:
                    self.lightSource.remove(map)
                self.lightSource.setIntensity(lightIntensity)
                if self.on:
                    self.lightSource.apply(map, pos)
            if lightRGB != self.lightRGB:
                self.lightSource.setRGB(lightRGB)

            self.lightRGB, self.lightIntensity = lightRGB, lightIntensity

        elif event.id == "enter tile":
            obj, epos, emap = event.args
            if obj is self or obj is self.carrier:
                parole.debug('******************* map = %r, emap = %r', map, emap)
                #assert(emap is map)
                if self.on:
                    self.lightSource.apply(emap, epos)
                    emap.update()
            if self.carrier and self.carrier.parentTile:
                if self.carrier.tileIsBrightEnough(self.carrier.parentTile):
                    self.carrier.goUnblind()

        elif event.id == 'enter inventory':
            item, creature = event.args
            if item is self:
                if self.on:
                    if not map:
                        map = creature.parentTile.map
                    #parole.debug('***************************** applying')
                    #self.lightSource.apply(map, creature.pos)
                    #map.update()
                    #if main.mapFrame.fovObj:
                    #    main.mapFrame.touchFOV()
                    self.enteringInventory = True

        self.updateName()


#=========================================

class Scroll(Item):
    """
    An item that can be read.
    """

    scrollWeight = 0.2  # pounds

    def __init__(self, name, char, rgb=None):
        Item.__init__(self, 'scroll of '+name, 10, parole.map.AsciiTile(char, 
            rgb or (255,255,255)), Scroll.scrollWeight)

    def listen(self, event):
        """
        Handles:
        """
        Item.listen(self, event)

    def read(self, reader):
        parole.error('Unimplemented scroll read: %r', self)
        pass

#=========================================

class Potion(Item):
    """
    Quaff
    """
    potionWeight = 0.5  # pounds

    def __init__(self, name, char, rgb=None):
        Item.__init__(self, 'potion of '+name, 20, parole.map.AsciiTile(char, 
            rgb or (255,255,255)), Potion.potionWeight)

    def listen(self, event):
        """
        Handles:
        """
        Item.listen(self, event)

    def quaff(self, quaffer):
        parole.error('Unimplemented potion quaffed: %r', self)
        pass

#=========================================

class Creature(Obj):
    """
    A mobile character/creature. Has stats like HP, etc.
    """
    # Default slots available for worn items
    defaultWearSlots = ['body', 'head', 'arms', 'legs', 'feet', 'hands',
                        'weapon', 'shield' ]

    statNames = [
        'hp', 'xp', 'level',

        'maxHP', 'strength', 'dexterity', 'constitution', 'perception', 'speed',
        'stamina', 'maxStamina', 'staminaRegen', 'capacity', 'pv', 'ev', 'hit',
        'damageSkew', 'maxUnarmedDamage', 'minVisibleLight',

        'maxHPMod', 'strengthMod', 'dexterityMod', 'constitutionMod',
        'perceptionMod', 'speedMod', 'maxStaminaMod', 'staminaRegenMod',
        'capacityMod', 'pvMod', 'evMod', 'hitMod', 'damageSkewMod',
        'maxUnarmedDamageMod', 'minVisibleLightMod', ]

    # Base energy costs of actions
    walkEnergy = 2000
    toggleDoorEnergy = 2000
    pickupBaseEnergy = 2000 # energy to pickup = base + ceof*weight
    pickupWeightCoefEnergy = 100
    dropEnergy = 1000
    unarmedAttackEnergy = 1000

    energyPerStaminaLoss = 5000 # doing this much work reduces stamina by 1
    regenLossTime = 15*60000 # every this many ms, the stamina regen rate
                             # decreases by 1 (to a minimum of zero)

    def __init__(self, name, shader, strength, dexterity, constitution,
            perception, speed, level, startsVowel=False, wearslots=None,
            unarmedVerb=None, dieVerb='die', leavesCorpse=True,
            unarmedAttackEnergy=None):
        Obj.__init__(self, name, 10000, shader, startsVowel=startsVowel,
                blockMove=True)

        self.dead = False
        self.blind = False # not actually blindness, just inability to see own
                           # tile
        self.dieVerb = dieVerb
        self.leavesCorpse = leavesCorpse
        self.lastDamageSource = None
        if unarmedAttackEnergy:
            self.unarmedAttackEnergy = unarmedAttackEnergy
        self.energyToStaminaLoss = self.energyPerStaminaLoss

        # stat base values
        self.strength = strength
        self.dexterity = dexterity
        self.constitution = constitution
        self.perception = perception
        self.speed = speed
        self.level = level
        self.xp = 0

        # stat modifiers
        self.maxHPMod = 0
        self.strengthMod = 0
        self.dexterityMod = 0
        self.constitutionMod = 0
        self.perceptionMod = 0
        self.speedMod = 0

        self.maxHPMod = 0
        self.maxStaminaMod = 0
        self.staminaRegenMod = 0
        self.capacityMod = 0
        self.pvMod = 0
        self.evMod = 0
        self.hitMod = 0
        self.damageSkewMod = 0
        self.maxUnarmedDamageMod = 0
        self.minVisibleLightMod = 0

        # variable and derived stats
        self.setDerivedStatBases()

        # hp and stamina start at max
        self.hp = self.maxHP
        self.stamina = self.maxStamina

        # inventory and slots
        self.inventory = []
        wearslots = wearslots or self.defaultWearSlots
        self.wearSlots = dict([(ws, None) for ws in wearslots])

        # verb used for unarmed melee attacks
        # i would prefer punch, but that's harder to conjugate (epenthetic "e")
        self.unarmedVerb = unarmedVerb or 'sock'

        # setup stamina regeneration
        self.scheduleStaminaRegen()
        main.schedule.add(Event('decrease stat', 
                main.schedule.time + self.regenLossTime, 
                (self, 'staminaRegenMod', 1), dests=[self]))

    def setDerivedStatBases(self):
        self.maxHP = int(round(75*self['constitution']/10.)) + \
                     int(round(10*self['strength']/10.)) + \
                     int(round(10*(self.level+3)/4.)) + \
                     int(round(5*self['dexterity']/10.))
        self.maxStamina = int(round(50*self['constitution']/10.)) + \
                          int(round(20*self['strength']/10.)) + \
                          int(round(20*(self.level+3)/4.)) + \
                          int(round(10*self['dexterity']/10.))

        self.capacity = int(round(50 * self['strength']/10.))

        self.pv = 0
        self.ev = 10 + ((self['dexterity'] - 10)) + (self['speed'] - 10)/2
        self.hit = 10 + ((self['dexterity'] - 10)) + (self['speed'] - 10)/2

        self.damageSkew = 10 + (2*(self['strength'] - 10)) # just the skew

        # the assumption here is of weak, human-like unarmed dmg. monsters
        # should use maxUnarmedDamageMod to increase this for natural weapons,
        # etc.
        self.maxUnarmedDamage = max(0, 20 + 2*(self['strength'] - 10))

        # staminaRegen - this will really need to be tweaked. this is our rate
        # of regen per minute at the start of the day. over time staminaRegeMod
        # gets lowered. the rate at which that happens also needs to be tweaked
        self.staminaRegen = 17 + (self['constitution'] - 10)

        # actual visible intensity is this divided by 1000
        self.minVisibleLight = 40. - 1.8*(self['perception']-10)

    def __getitem__(self, stat):
        try:
            mod = getattr(self, stat+'Mod')
            return max(0, getattr(self, stat) + mod)
        except AttributeError:
            return getattr(self, stat)

    def timeOfAction(self, energy, speedNudge=0):
        rate = (float(self['speed'] + speedNudge) / 10.0) #* 1000.0
        time = float(energy) / rate
        return int(round(time))

    def inventoryWeight(self):
        return sum([item.weight for item in self.inventory])

    def inventoryCapacity(self):
        return self['capacity']

    def visibleObjectsAt(self, tile):
        # TODO: visibility
        return [x for x in tile]

    def tileIsBrightEnough(self, tile):
        return magnitude(tile.availLight) >= self['minVisibleLight']

    def statDeath(self):
        if self['strength'] < 0 or self['constitution'] < 0 or \
                self['dexterity']< 0 or self['speed'] < 0:
            return True
        return False

    def doFatigue(self, energy):
        self.energyToStaminaLoss -= energy
        if self.energyToStaminaLoss <= 0:
            self.energyToStaminaLoss = self.energyPerStaminaLoss + \
                                       self.energyToStaminaLoss
            main.schedule.add(Event('decrease stat', main.schedule.time, 
                    (self, 'stamina', 1), dests=[self]))

    def scheduleStaminaRegen(self):
        # stamina regen rate is in stamina points per minute (=60,000 ms)
        # so we solve for the time of a 1-point increase in stamina
        regenTime = main.schedule.time + int(round(60000./self['staminaRegen']))
        main.schedule.add(Event('regen stamina', regenTime, (self,),
            dests=[self]))

    def maintainStats(self):
        # check whether hp went below 0. this is the only place that
        # checks this! so something will only die if its hp drops below
        # 0 as the result of a 'set stat' event.
        if self['hp'] <= 0 or self.statDeath():
            self.die()
        else:
            # reset the bases of our derived stats to react to any changes
            self.setDerivedStatBases()

        # clamp hp to maxHP
        if self['maxHP'] < self['hp']:
            main.schedule.add(Event('set stat', main.schedule.time,
                    (self, 'hp', self['maxHP']), dests=[self])) 
        # clamp stamina to maxStamina
        if self['maxStamina'] < self['stamina']:
            main.schedule.add(Event('set stat', main.schedule.time,
                    (self, 'stamina', self['maxStamina']), dests=[self])) 


    def listen(self, event):
        """
        Handles:
            "enter inventory"  (item, creature)
            "leave inventory"  (item, creature)
            "wear"             (item, creature, slot)
            "remove"           (item, creature, slot)
            "set stat"         (creature, statname, value)
            "increase stat"    (creature, statname, amount)
            "decrease stat"    (creature, statname, amount)
            "receive attack"   (target, source, weapon)
            "receive damage"   (target, source, weapon, amount)
            "enter tile"       (obj, pos, map)
            "regen stamina"    (creature)
            "quaff"             (creature, potion)
            "read"             (creature, scroll)
        """

        if self.dead:
            # we handle nothing if we are dead, not even events for the
            # superclass
            # subclasses of Creature should do the same
            return

        Obj.listen(self, event)
        map = None
        if self.parentTile:
            map = self.parentTile.map

        if event.id == "enter inventory":
            item, creature = event.args
            if creature is self:
                #parole.debug('%r', event)
                #main.schedule.add(Event('leave tile', main.schedule.time,
                #        (item, self.pos), dests=[item]))
                map[self.pos].remove(item)
                self.inventory.append(item)
                assert(not item.carrier) # must leave another creature's inv
                                         # before entering this one's
                item.carrier = self
                if creature is main.player:
                    main.transientAnnotes.append(main.mapFrame.annotate(
                        creature.parentTile,
                        "You pick up %s." % item.noun(definite=1)))
                elif visible(creature):
                    main.transientAnnotes.append(main.mapFrame.annotate(
                        creature.parentTile,
                        "%s picks up %s." % (creature.noun(definite=1,
                            capitalized=1), item.noun(definite=1))))

        elif event.id == "leave inventory":
            assert(map)
            item, creature = event.args
            if creature is self:
                if isinstance(item, Wearable) and item.wearer:
                    if creature is main.player:
                        main.transientAnnotes.append(main.mapFrame.annotate(
                            creature.parentTile,
                            "You must remove %s first." % item.noun(definite=1)))
                    return

                self.inventory.remove(item)
                item.carrier = None
                map[self.pos].add(item)
                main.schedule.add(Event('enter tile', main.schedule.time,
                        (item, self.pos, map), dests=[item]))
                if creature is main.player:
                    main.transientAnnotes.append(main.mapFrame.annotate(
                        creature.parentTile,
                        "You drop %s." % item.noun(definite=1)))
                elif visible(creature):
                    main.transientAnnotes.append(main.mapFrame.annotate(
                        creature.parentTile,
                        "%s drops %s." % (creature.noun(definite=1,
                            capitalized=1), item.noun(definite=0))))

        elif event.id == 'wear':
            item, creature, slot = event.args
            if creature is self:
                # "wield" verb for weapons
                if slot == 'weapon':
                    verb = 'wield'
                elif slot == 'shield':
                    verb = 'ready'
                else:
                    verb = 'don'
                if creature is main.player:
                    main.transient("You %s %s." % (verb,
                        item.noun(definite=True)))
                elif visible(creature):
                    main.transient("%s %ss %s." % (creature.noun(definite=True,
                        capitalized=True), verb, item.noun()), creature.parentTile)

                self.wearSlots[slot] = item
                item.wearer = self
                item.wearerSlot = slot

        elif event.id == 'remove':
            item, creature, slot = event.args
            if creature is self:
                self.wearSlots[slot] = None
                item.wearer = None
                item.wearerSlot = None

                # "unwield" verb for weapons
                if slot == 'weapon':
                    verb = 'unwield'
                else:
                    verb = 'remove'
                if creature is main.player:
                    main.transient("You %s %s." % (verb,
                        item.noun(definite=True)))
                elif visible(creature):
                    main.transient("%s %ss %s." % (creature.noun(definite=True,
                        capitalized=True), verb, item.noun()), creature.parentTile)

        elif event.id == 'set stat':
            creature, statname, value = event.args
            if creature is self:
                if statname not in self.statNames:
                    raise ValueError('Got "set stat" event with bad statname '
                            '%r' % statname)
                newVal = int(round(value))
                if 'Mod' not in statname and statname not in ('hp', 'stamina'):
                    newVal = max(0, newVal)
                setattr(self, statname, newVal) 

                self.maintainStats()

        elif event.id in ('increase stat', 'decrease stat'):
            creature, statname, amount = event.args
            if creature is self:
                if statname not in self.statNames:
                    raise ValueError('Got %r event with bad statname '
                            '%r' % (event.id, statname))
                curStat = getattr(self, statname)

                # NO, BAD!
                #main.schedule.add(Event('set stat', main.schedule.time,
                #    (self, statname, 
                #     curStat + (event.id[0]=='i' and 1 or -1)*amount),
                #    dests=[self]))
                # instead:
                change = (event.id[0]=='i' and 1 or -1)*amount
                newVal = int(round(curStat+change))
                if 'Mod' not in statname and statname != 'stamina':
                    newVal = max(0, newVal)
                setattr(self, statname, newVal)
                self.maintainStats()

                # continue the stamina regen depreciation cycle
                if statname == 'staminaRegenMod' and \
                        event.id == 'decrease stat':
                    main.schedule.add(Event('decrease stat', 
                        main.schedule.time + self.regenLossTime, 
                        (self, 'staminaRegenMod', 1), dests=[self]))

        elif event.id == 'receive attack':
            # currently do nothing.
            parole.debug('%r received attack from %r wielding %r' % event.args)

        elif event.id == 'receive damage':
            # TODO: in the future, only certain kinds of damage (i.e. physical)
            # should be reduced by pv.
            target, source, weapon, amount = event.args
            if target is self:
                protection = self.protectionDraw()
                amount -= protection
                if amount <= 0:
                    # TODO: absorbed damage message
                    if self is main.player:
                        main.transient('Your armor absorbs all the damage!')
                    elif visible(self):
                        main.transient('%s armor absorbs all the damage!' % \
                                self.pronounGen(capitalized=1), self.parentTile)
                    return
                parole.debug('%r received %s damage.', self, amount)
                main.schedule.add(Event('decrease stat', main.schedule.time,
                        (self, 'hp', amount), dests=[self]))
                self.lastDamageSource = source

        elif event.id == 'enter tile':
            obj, pos, map = event.args
            if obj is self:
                # if we're entering a tile that is too dim to see in, apply
                # blindness penalties
                if not self.tileIsBrightEnough(self.parentTile):
                    self.goBlind()

                # if we're entering a tile that is bright enough to see in, 
                # remove blindness penalties
                else:
                    self.goUnblind()

        elif event.id == 'regen stamina':
            creature = event.args[0]
            if creature is self:
                if self['stamina'] < self['maxStamina']:
                    main.schedule.add(Event('increase stat', main.schedule.time,
                            (self, 'stamina', 1), dests=[self]))
                self.scheduleStaminaRegen()

        elif event.id == 'quaff':
            creature, potion = event.args
            if creature is self:
                potion.quaff(self)

        elif event.id == 'read':
            creature, scroll = event.args
            if creature is self:
                scroll.read(self)


    # actions. return scheduled time of completion or -1 if action failed and
    # was not scheduled
    def toggleDoor(self, door):
        # Make sure the door is adjacent
        if door.parentTile not in adjacentTiles(self.parentTile):
            return -1


        time = main.schedule.time + self.timeOfAction(self.toggleDoorEnergy)
        self.doFatigue(self.toggleDoorEnergy)
        main.schedule.add(Event('door', 
            time, (door, 'toggle'), dests=[door]))
        parole.debug('%s: toggling door at %d', self.noun(), time)
        return time

    def pickupItem(self, item):
        # make sure the item is in our tile
        if item.parentTile is not self.parentTile:
            main.transientAnnotes.append(main.mapFrame.annotate(item.parentTile, 
                "%s is too far away to pick up." % item.noun(definite=True,
                    capitalized=True)))
            return -1

        # make sure the item is not too heavy
        if item.weight + self.inventoryWeight() > self.inventoryCapacity():
            main.transientAnnotes.append(main.mapFrame.annotate(item.parentTile, 
                "%s is too heavy to carry with your other items." \
                 % item.noun(definite=True, capitalized=True)))
            return -1

        energy = self.pickupBaseEnergy + self.pickupWeightCoefEnergy*item.weight
        time = main.schedule.time + self.timeOfAction(energy)
        self.doFatigue(energy)
        main.schedule.add(Event('enter inventory', time, (item, self),
            dests=[self, item]))
        #parole.debug('will pickup at %d', time)
        return time

    def dropItem(self, item):
        # make sure the item is in our inventory
        if item.carrier is not self:
            # how would this ever happen?
            main.transientAnnotes.append(main.mapFrame.annotate(item.parentTile, 
                "%s is not in your inventory!" % item.noun(definite=True,
                    capitalized=True)))
            return -1
        assert(item in self.inventory)

        time = main.schedule.time + self.timeOfAction(self.dropEnergy)
        self.doFatigue(self.dropEnergy)
        main.schedule.add(Event('leave inventory', time, (item, self),
            dests=[self, item]))
        return time

    def wear(self, item, slot=None):
        # make sure the item is in our inventory
        if item.carrier is not self:
            # how would this ever happen?
            main.transientAnnotes.append(main.mapFrame.annotate(item.parentTile, 
                "%s is not in your inventory!" % item.noun(definite=True,
                    capitalized=True)))
            return -1
        assert(item in self.inventory)
        if not isinstance(item, Wearable):
            # how would this ever happen?
            main.transientAnnotes.append(main.mapFrame.annotate(self.parentTile, 
                "%s is not wearable or wieldable!" % item.noun(definite=True,
                    capitalized=True)))
            return -1

        slot = slot or item.wearSlots[0]
        if self.wearSlots[slot]:
            # player has to remove something first
            if slot == 'weapon':
                verb = 'unwield'
            else:
                verb = 'remove'
            main.transient('You must first %s %s.' %
                    (verb, self.wearSlots[slot].noun(),), self.parentTile)
            return -1

        time = main.schedule.time + self.timeOfAction(item.wearEnergy)
        self.doFatigue(item.wearEnergy)
        main.schedule.add(Event('wear', time, (item, self, slot),
            dests=[self, item]))
        return time

    def remove(self, item):
        slot = None
        for s in self.wearSlots:
            if self.wearSlots[s] is item:
                slot = s
                break
        if not slot:
            main.transient('You are not wearing or wielding %s!' %
                    (item.noun(definite=1),), self.parentTile)
            return -1

        time = main.schedule.time + self.timeOfAction(item.wearEnergy)
        self.doFatigue(item.wearEnergy)
        main.schedule.add(Event('remove', time, (item, self, slot),
            dests=[self, item]))
        return time

    def melee(self, targetCreature):
        if targetCreature.parentTile not in adjacentTiles(self.parentTile):
            # how did this happen?
            parole.error('Creature "%r" tried melee nonadjacent target "%r"',
                    self, targetCreature)
            parole.error('target.parentTile: %r, self.parentTile: %r',
                    targetCreature.parentTile, self.parentTile)
            assert(False)

        # get info from weapon
        if ('weapon' in self.wearSlots) and (self.wearSlots['weapon']):
            weapon = self.wearSlots['weapon']
            verb = weapon.verb
            attackEnergy = weapon.attackEnergy
        else:
            weapon = None
            verb = self.unarmedVerb
            attackEnergy = self.unarmedAttackEnergy

        # when will we be done attacking?
        attackTime = main.schedule.time + self.timeOfAction(attackEnergy)
        self.doFatigue(attackEnergy)

        # schedule a "receive attack" event, regardless of whether we hit
        main.schedule.add(Event("receive attack", attackTime, (targetCreature,
            self, weapon), dests=[targetCreature]))

        # see whether our attack draw beats the target's defense draw
        if self.attackDraw(targetCreature) > targetCreature.defenseDraw(self):
            # it's a hit
            if main.player is self:
                main.transient('You %s %s.' % (verb,
                    visible(targetCreature) and targetCreature.noun(definite=1)\
                            or 'something'))
            elif main.player is targetCreature:
                main.transient('%s %s you.' % \
                        (visible(self) and self.noun(definite=1,
                    capitalized=1) or 'Something', thirdperson(verb)),
                        self.parentTile)

            # draw damage
            if weapon:
                damage = weapon.drawDamage(self, targetCreature)
            else:
                damage = self.drawUnarmedDamage(targetCreature)

            # schedule a "receive damage" event
            main.schedule.add(Event("receive damage", attackTime,
                (targetCreature, self, weapon, damage), dests=[targetCreature]))
        else:
            # it's a miss
            if main.player is self:
                main.transient('You miss %s.' % \
                        (visible(targetCreature) and \
                        targetCreature.noun(definite=1) or 'something',))
            elif main.player is targetCreature:
                main.transient('%s misses you.' % \
                        (visible(self) and self.noun(definite=1, capitalized=1) \
                        or 'Something',),
                        self.parentTile)

        return attackTime

    def die(self, dieVerb=None, corpsePossible=True):
        # causes the creature to die immediately, regardless of hp
        # removes the creature from its tile, creates a corpse, adds the corpse
        # to the tile, causes the creature to stop handling any events
        self.dead = True
        if self is main.player:
            main.transient('You %s.' % (dieVerb or self.dieVerb,))
        else:
            main.transient('%s %s.' % \
                    (visible(self) and self.noun(definite=1, capitalized=1) \
                     or 'Something',
                    thirdperson(dieVerb or self.dieVerb)),
                    self.parentTile)

        if corpsePossible:
            corpse = self.makeCorpse()
            if corpse:
                self.parentTile.add(corpse)
        self.parentTile.remove(self)

    def goBlind(self):
        if self.blind:
            return
        self.blind = True
        #main.schedule.add(Event('decrease stat', main.schedule.time, 
        #        (self, 'evMod', 1), dests=[self]))
        #main.schedule.add(Event('decrease stat', main.schedule.time, 
        #        (self, 'hitMod', 1), dests=[self]))
        main.schedule.add(Event('decrease stat', main.schedule.time, 
                (self, 'speedMod', 2), dests=[self]))
        #if self is main.player:
        #    main.transient("You can't see where you are.")

    def goUnblind(self):
        if not self.blind:
            return
        self.blind = False
        #main.schedule.add(Event('increase stat', main.schedule.time, 
        #        (self, 'evMod', 1), dests=[self]))
        #main.schedule.add(Event('increase stat', main.schedule.time, 
        #        (self, 'hitMod', 1), dests=[self]))
        main.schedule.add(Event('increase stat', main.schedule.time, 
                (self, 'speedMod', 2), dests=[self]))
        #if self is main.player:
        #    main.transient("You can see yourself again.")

    def makeCorpse(self):
        if not self.leavesCorpse:
            return None

        corpse = Obj(self.name + ' corpse', 5, parole.map.AsciiTile('%',
            self.shader.rgb), startsVowel=self.startsVowel)
        return corpse

    def attackDraw(self, targetCreature):
        # unseen attacker bonus
        unseenBonus = 0
        if targetCreature is main.player and not visible(self):
            unseenBonus = 10
        return random.betavariate(self['hit']+unseenBonus, 10)

    def defenseDraw(self, attacker):
        # TODO: blindness effects
        return random.betavariate(max(1, self['ev']), 10)

    def protectionDraw(self):
        # so, mean protection will be pv
        return random.betavariate(10, 10) * 2 * self['pv']

    def drawUnarmedDamage(self, target):
        # at some point might want to be sensitive to who the target is
        return random.betavariate(max(1, self['damageSkew']), 10) \
                * self['maxUnarmedDamage']

#=========================================

class Monster(Creature):
    """
    A Monster is a Creature that is always assumed hostile to the player, and
    which has some sort of fighting AI
    """
    def __init__(self, name, shader, strength, dexterity, constitution,
            perception, speed, level, xpValue, startsVowel=False,
            wearslots=None, unarmedVerb=None, dieVerb='die', leavesCorpse=True,
            unarmedAttackEnergy=None, description=None, brain=None):

        Creature.__init__(self, name, shader, strength, dexterity, constitution,
                perception, speed, level, startsVowel, wearslots, unarmedVerb,
                dieVerb, leavesCorpse, unarmedAttackEnergy)
        self.xpValue = xpValue
        self.description = description
        self.brain = brain or ai.DefaultBrain(self, ai.defaultStateMap)

    def die(self, dieVerb=None, corpsePossible=True):
        Creature.die(self, dieVerb, corpsePossible)

        # give xp to our killer
        if isinstance(self.lastDamageSource, Creature):
            main.schedule.add(Event('increase stat', main.schedule.time,
                (self.lastDamageSource, 'xp', self.xpValue),
                dests=[self.lastDamageSource]))

    def listen(self, event):
        """
        Passes events on to Creature and to this creature's brain.
        """
        Creature.listen(self, event)
        self.brain.listen(event)

    def noun(self, definite=False, proper=False, capitalized=False):
        n = Creature.noun(self, definite, proper, capitalized)
        hpPcnt = float(self['hp']) / float(self['maxHP'])
        status = None
        if hpPcnt == 1.0:
            status = 'uninjured'
        elif hpPcnt > 0.8:
            status = 'lightly injured'
        elif hpPcnt > 0.6:
            status = 'moderately injured'
        elif hpPcnt > 0.4:
            status = 'quite injured'
        elif hpPcnt > 0.2:
            status = 'gravely injured'
        else:
            status = 'deathly injured'
        return '%s (%s)' % (n, status)

#=========================================

class NPC(Creature):
    """
    An NPC is a Creature that is friendly to the player and can talk. Can't
    attack or be attacked.
    """
    pass

#=========================================

class Player(Creature):
    """
    The Creature that the player actually controls.
    """

    startingKit = [
        'shortsword',    # these refer to script names
        'shirt',
        'cap',
        'sandals',
        'torch',
        'healing',
        'healing',
        'greaterhealing',
    ]

    levelXP = {
            2:  180,
            3:  360,
            4:  720,
            5:  1440,
            6:  3200*.9,
    }

    maxSparkWeight = 5 # can't light anything over this weight without using a
                   # torch/lantern
    snuffEnergy = 2000
    quaffEnergy = 2000
    readEnergy = 1000
    
    def __init__(self, playerName='Unknown Player', str=10, dex=10, con=10,
            per=10, spd=10):
        Creature.__init__(self, "player", parole.map.AsciiTile('@',
            (255,255,255)), str, dex, con, per, spd, 1)
        self.layer = 12000
        self.playerName = playerName

        self.acquireStartingKit()

        # wizard/debug mode stuff
        self.monsterReporting = False
        self.clipping = True

        self.winner = False
        self.encounteredMonsters = set()
        self.levelsSeen = set()

        # TODO: this should be subsumed by a general effects tracker
        self.farsight = False

    def acquireStartingKit(self):
        for script in self.startingKit:
            for type in ('weapons', 'armor', 'scrolls', 'potions', 'lights'):
                itemClass = parole.resource.getObject(
                    'scripts/%s/%s.py' % (type, script), 
                    'thingClass')
                if itemClass:
                    break

            if not itemClass:
                assert(False)

            item = itemClass()
            self.inventory.append(item)
            item.carrier = self

            if isinstance(item, Wearable):
                slot = item.wearSlots[0]
                if not self.wearSlots[slot]:
                    self.wearSlots[slot] = item
                    item.wearer = self
                    item.wearerSlot = slot
                    # let item apply bonuses
                    main.schedule.add(Event('wear', main.schedule.time,
                        (item, self, slot), dests=[item]))

    def light(self, target):
        if not isinstance(target, LightSource):
            raise TypeError('target must be a LightSource')
        if target.on:
            main.transient('%s is already lit!' % target.noun(definite=1,
                capitalized=1))
            return -1

        #haveTorch = self.wearSlots['light'] and self.wearSlots['light'].on
        source = None
        for obj in self.inventory:
            if isinstance(obj, LightSource) and obj.on:
                source = obj
        if not source and target.weight > self.maxSparkWeight:
            main.transient('%s is too big to light with your flint. '
                           "You'll need a torch or something." % \
                                   target.noun(definite=1, capitalized=1))
            return -1


        energy = target.lightEnergy
        if source:
            energy /= 2

        time = main.schedule.time + self.timeOfAction(energy)
        self.doFatigue(energy)

        main.schedule.add(Event('activate light', time, (target,),
            dests=[target]))
        return time

    def snuff(self, target):
        if not isinstance(target, LightSource):
            raise TypeError('target must be a LightSource')
        if not target.on:
            main.transient('%s is already unlit!' % target.noun(definite=1,
                capitalized=1))
            return -1

        time = main.schedule.time + self.timeOfAction(self.snuffEnergy)
        self.doFatigue(self.snuffEnergy)

        main.schedule.add(Event('deactivate light', time, (target,),
            dests=[target]))
        return time

    def quaff(self, target):
        time = main.schedule.time + self.timeOfAction(self.quaffEnergy)
        self.doFatigue(self.quaffEnergy)

        main.schedule.add(Event('quaff', time, (self, target,),
            dests=[self]))

        self.inventory.remove(target)
        return time

    def read(self, target):
        time = main.schedule.time + self.timeOfAction(self.readEnergy)
        self.doFatigue(self.readEnergy)

        main.schedule.add(Event('read', time, (self, target,),
            dests=[self]))

        self.inventory.remove(target)
        return time

    def bindMe(self):
        if self.farsight:
            return
        if main.mapFrame.fovObj is not self or \
                main.mapFrame.fovRad != self['perception']:
            main.mapFrame.bindVisibilityToFOV(self, self['perception'],
                    remember=False, fovCondition=self.tileIsBrightEnough)
            main.mapFrame.getMap().update()

    def listen(self, event):
        """
        Handles:
            - "walk" ("north"/"south" + "east"/"west")
            - "enter tile" (obj, dest, map)
        """
        Creature.listen(self, event)
        #parole.debug('Player got event: %r', event)
        map = None
        if self.parentTile:
            map = self.parentTile.map

        if event.id == 'walk':
            assert(map)
            dx, dy = displacement(event.args[0])
            dest = self.pos[0] + dx, self.pos[1] + dy
            if dest[0] < 0 or dest[0] >= map.cols or dest[1] < 0 or \
                    dest[1] >= map.rows:
                # destination is oob; continue waiting for a command
                main.schedule.add(Event('get command', main.schedule.time))
                return

            # if there's a monster in the destination, we're going to attack
            # rather than move
            for obj in map[dest]:
                if isinstance(obj, Monster):
                    # It's assumed there can be only one monster in a tile
                    main.schedule.add(Event('get command', self.melee(obj)+1))
                    return

            if (not self.clipping) or (not map[dest].hasMoveBlocker()):
                # destination is not blocked; schedule translocation thereto
                time = main.schedule.time + self.timeOfAction(self.walkEnergy)
                self.doFatigue(self.walkEnergy)
                main.schedule.add(Event('translocate', main.schedule.time, 
                    args=(self, dest), dests=[self]))
                main.schedule.add(Event('get command', time + 1))
                return
            else:
                # destination is blocked; continue waiting for a command
                main.schedule.add(Event('get command', main.schedule.time))
                if not visible(dest):
                    main.transient('You feel a wall here.', dest)
            return
        elif event.id == 'enter tile':
            obj, dest, map = event.args
            if obj is self:
                # we've moved, so recenter the view on us
                main.mapFrame.centerAtTile(self.pos)
                main.mapFrame.selectTile(self.pos)

                # bind visibility
                if not main.mapFrame.fovObj and not parole.conf.options.wizard:
                    self.bindMe()


        elif event.id == 'set stat':
            creature, statname, value = event.args
            if (creature is self) and (statname in ('perception',
                    'perceptionMod')) :
                # our perception has changed, so rebind the view with new fov
                # radius
                if self.parentTile:
                    m = interface.messageBox('Patience...')
                    parole.display.update()
                    self.bindMe()
                    parole.display.scene.remove(m)

        elif event.id == 'translocate':
            obj, dest = event.args
            map = self.parentTile.map
            if obj is self:
                items = []
                for x in map[dest]:
                    if isinstance(x, Item):
                        items.append(x)

                if not visible(dest):
                    if len(items)==1:
                        main.transient('There is an item here', dest)
                    elif len(items) > 1:
                        main.transient('There are some items here', dest)
                else:
                    if len(items)==1:
                        main.transient('There is %s here' % items[0], dest)
                    elif len(items) > 1:
                        main.transient('There are some items here', dest)
    

        #parole.debug('Event ignored.')
            
